<?php

    #-------------------------------------------------------------#
	# This file is part of the Tekuna MVC Microframework for PHP. #
	# (c) 2009 Niels Jäckel <niels.jaeckel@tekuna.org>            #
	# Please visit http://tekuna.org/ for more information.       #
	#                                                             #
	# This software is licensed under the MIT License. Please     #
	# view the LICENSE file for the full text of this license.    #
    #-------------------------------------------------------------#


	/**
	 * Generic superclass for all Tekuna exceptions. It adds a cause field to
	 * the PHP standard Exception. So exception causes can be set and delivered.
	 * The __toString() method is adjusted appropriately
	 */

	class TekunaException extends Exception {

		protected $objCause = NULL;


		/**
		 * Build new instance of a TekunaException. The second parameter is added. There the exception
		 * cause can be provided as previous exception object.
		 *
		 * @param string $sMessage the exception message
		 * @param Exception $objCause the exception's cause
		 * @param int $iCode an exception code
		 */

		public function __construct($sMessage = NULL, Exception $objCause = NULL, $iCode = 0) {

			// build parent exception
			parent :: __construct($sMessage, $iCode);

			// set the cause if given
			if ($objCause !== NULL) {

				$this -> setCause($objCause);
			}
		}


		/**
		 * Set the exception cause exception object
		 *
		 * @param Exception $objCause the cause
		 */

		public function setCause(Exception $objCause) {

			$this -> objCause = $objCause;
		}


		/**
		 * Return the exception's cause
		 *
		 * @return Exception
		 */

		public function getCause() {

			return $this -> objCause;
		}


		/**
		 * Check, if this exception has a cause
		 *
		 * @return boolean
		 */

		public function hasCause() {

			return ($this -> objCause !== NULL);
		}


		protected function getAdjustedFile() {

			// does nothing here, but subclasses may do something.
			// return the file as it is
			return $this -> getFile();
		}


		protected function getAdjustedLine() {

			// does nothing here, but subclasses may do something.
			// return the line as it is
			return $this -> getLine();
		}


		protected function getAdjustedTrace() {

			// does nothing here, but subclasses may do something.
			// return the trace as it is
			return $this -> getTrace();
		}


		/**
		 * Get a string representation of the exception. This method
		 * was overridden to use the static getTextOutput() method.
		 *
		 * @return string
		 */

		public function __toString() {

			return TekunaException :: getTextOutput($this);
		}


		/**
		 * Produces a nice textbased output for an exception
		 *
		 * @param Exception $objException an object of type exception
		 *
		 * @return string
		 */

		public static function getTextOutput(Exception $objException) {

			// get values
			$sType = get_class($objException);
			$iCode = $objException -> getCode();
			$sMessage = $objException -> getMessage();

			if ($objException instanceof TekunaException) {

				$sFile = $objException -> getAdjustedFile();
				$iLine = $objException -> getAdjustedLine();
				$arrTrace = $objException -> getAdjustedTrace();
			}
			else {

				$sFile = $objException -> getFile();
				$iLine = $objException -> getLine();
				$arrTrace = $objException -> getTrace();
			}

			// show first values
			$sOutput = $sType . (($iCode != 0) ? " (code: $iCode)" : '') ."\n";
			$sOutput .= "Message: $sMessage\n\n";
			$sOutput .= "File: $sFile\n";
			$sOutput .= "Line: $iLine\n";
			$sOutput .= "Trace:\n";

			// show trace
			foreach ($arrTrace as $i => $arrEntry) {

				$sFile = 'unknown file';
				$iLine = 'unknown line';

				if (isset($arrEntry['file']) && isset($arrEntry['line'])) {

					$sFile = $arrEntry['file'];
					$iLine = $arrEntry['line'];
				}

				$sFunction = TekunaException :: formatTraceFunction($arrEntry);
				
				$sArguments = '';
				if (isset($arrEntry['args'])) {
				
					$sArguments = TekunaException :: formatTraceArguments($arrEntry['args']);
				}
				
				$sOutput .= "\t#$i\t$sFunction ($sArguments)\n";
				$sOutput .= "\t\t\tFile: $sFile\n";
				$sOutput .= "\t\t\tLine: $iLine\n\n";
			}

			// append last 'main' function line
			$i++;
			$sOutput .= "\t#$i\t{main}\n";

			// only TekunaExceptions have causes
			if ($objException instanceof TekunaException) {

				// recurse into the causing exception, if given
				if ($objException -> hasCause()) {

					$objCausingException = $objException -> getCause();

					$sOutput .= "\n\nCaused by: ";
					$sOutput .= TekunaException :: getTextOutput($objCausingException);
				}
			}

			return $sOutput;
		}


		/**
		 * Produces a nice XHTML output for an exception
		 *
		 * @param Exception $objException an object of type exception
		 *
		 * @return string
		 */

		public static function getXHTMLOutput(Exception $objException) {

			// get values
			$sType = get_class($objException);
			$iCode = $objException -> getCode();
			$sMessage = $objException -> getMessage();
			$sFile = $objException -> getAdjustedFile();
			$iLine = $objException -> getAdjustedLine();
			$arrTrace = $objException -> getAdjustedTrace();

			// show first values
			$sCode = ($iCode != 0) ? " (code: $iCode)" : '';

			$sOutput = "\n<div class=\"exception\">\n";
			$sOutput .= "\t<h1>Uncaught Exception: {$sType}{$sCode}</h1>\n";
			$sOutput .= "\t<p>{$sMessage}</p>\n";
			$sOutput .= "\t<p>[Thrown on line {$iLine} in {$sFile}]</p>\n";
			$sOutput .= "\t<h2>Trace:</h2>\n";
			$sOutput .= "\t<dl>\n";

			// show trace
			foreach ($arrTrace as $i => $arrEntry) {

				$sFile = $arrEntry['file'];
				$iLine = $arrEntry['line'];
				$sFunction = TekunaException :: formatTraceFunction($arrEntry);

				$sArguments = '';
				if (isset($arrEntry['args'])) {
				
					$sArguments = TekunaException :: formatTraceArguments($arrEntry['args']);
				}

				$sOutput .= "\t\t<dt>";
				$sOutput .= "#$i&emsp;$sFunction( $sArguments )";
				$sOutput .= "</dt>\n\t\t<dd>";
				$sOutput .= "&emsp;Line: $iLine<br />\n\t\t";
				$sOutput .= "&emsp;File: $sFile<br /><br />";
				$sOutput .= "</dd>\n";
			}

			// append last 'main' function line
			$i++;

			$sOutput .= "\t\t<dt>";
			$sOutput .= "#$i&emsp;{main}";
			$sOutput .= "</dt>\n\t\t<dd></dd>\n";
			$sOutput .= "\t</dl>\n";

			// end of trace div
			$sOutput .= "</div>\n";

			// only TekunaExceptions have causes
			if ($objException instanceof TekunaException) {

				// recurse into the causing exception, if given
				if ($objException -> hasCause()) {

					$objCausingException = $objException -> getCause();

					$sOutput .= "<p>Caused by:</p>";
					$sOutput .= TekunaException :: getXHTMLOutput($objCausingException);
				}
			}


			return $sOutput;
		}


		protected static function formatTraceFunction(array $arrTraceEntry) {

			// init function
			$sFunction = '';

			// if the class is available show it
			if (isset($arrTraceEntry['class'])) {

				$sFunction .= $arrTraceEntry['class'] .' ';
			}

			// show the calling type if available
			if (isset($arrTraceEntry['type'])) {

				$sFunction .= $arrTraceEntry['type'] .' ';
			}

			// append the actually called function
			$sFunction .= $arrTraceEntry['function'];

			return $sFunction;
		}


		protected static function formatTraceArguments(array $arrTraceEntryArguments) {

			// build arguments list
			$arrArguments = array();

			// iterate all the arguments
			foreach ($arrTraceEntryArguments as $mArg) {

				switch (gettype($mArg)) {

					// give only the count with arrays
					case 'array': $arrArguments[] = 'Array['. count($mArg) .']'; break;

					// maximum 300 characters for strings
					case 'string':

						if (strlen($mArg) > 300) {

							$mArg = substr($mArg, 0, 300);
							$mArg .= '...';
						}

						$arrArguments[] = "'$mArg'";
						break;

					// show objects with type
					case 'object': $arrArguments[] = get_class($mArg) .' Object'; break;

					// show explicit NULL values
					case 'NULL': $arrArguments[] = 'NULL'; break;

					// translate bool values
					case 'boolean': $arrArguments[] = (($mArg) ? 'TRUE' : 'FALSE'); break;

					// visualize an unknown type
					case 'unknown type': $arrArguments[] = '[?]'; break;

					// default handler for int, float, resource
					default: $arrArguments[] = (string) $mArg; break;
				}
			}

			// put all arguments together
			return implode(', ', $arrArguments);
		}
	}
